﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ScatterChart.cs" company="NoCopmany">
//   
// </copyright>
// <summary>
//   The scatter chart.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using Graphix.Elements;
using Graphix.Helpers;

namespace Graphix.Charts
{
    using CommonHelpers;

    using Graphix.Charts.Base;


    /// <summary>
    /// The scatter chart.
    /// </summary>
    public class ScatterChart : BothAxisAndLegend<float>
    {
        #region Fields
        /// <summary>
        /// Names of the series to draw.
        /// </summary>
        private List<string> seriesNames = new List<string>();

        /// <summary>
        /// The x values.
        /// </summary>
        private List<float> xValues = new List<float>();

        /// <summary>
        /// The y values.
        /// </summary>
        private Dictionary<float, List<float>> yValues = new Dictionary<float, List<float>>();
        #endregion


        #region Properties

        /// <summary>
        /// Gets or sets a value indicating whether X and Y axis are scaled nicely.
        /// </summary>
        public bool ScaleNicely { get; set; }

        #region Handling YSeries
        /// <summary>
        /// Adds name of Y axis series
        /// </summary>
        /// <param name="name">
        /// one of Y axis series name
        /// </param>
        public void AddYSeriesName(string name)
        {
            this.seriesNames.Add(name);
        }

        /// <summary>
        /// The edit y series name.
        /// </summary>
        /// <param name="seriesNumber">
        /// The series number.
        /// </param>
        /// <param name="name">
        /// New name of series with that number.
        /// </param>
        /// <exception cref="ArgumentException">
        /// Throws when series with that number doesn't exist.
        /// </exception>
        public void EditYSeriesName(int seriesNumber, string name)
        {
            if (seriesNumber < 0 || this.seriesNames.Count > seriesNumber)
            {
                this.seriesNames[seriesNumber] = name;
            }
            else
            {
                throw new ArgumentException(string.Format("Series #{0} doesn't exist.", seriesNumber));
            }
        }

        /// <summary>
        /// The edit y series name.
        /// </summary>
        /// <param name="oldName">
        /// The series old name.
        /// </param>
        /// <param name="newName">
        /// New name of that series.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/> which indicates success.
        /// </returns>
        public bool EditYSeriesName(string oldName, string newName)
        {
            int loop = this.seriesNames.Count;
            for (int i = 0; i < loop; i++)
            {
                if (this.seriesNames[i] == oldName)
                {
                    this.seriesNames[i] = newName;
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// The remove last series name.
        /// </summary>
        /// <returns>
        /// The <see cref="bool"/> which indicates success.
        /// </returns>
        public bool RemoveLastSeriesName()
        {
            int number = this.seriesNames.Count - 1;
            if (number > 0)
            {
                this.seriesNames.RemoveAt(number);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Removes series with given name.
        /// </summary>
        /// <param name="name">
        /// Name of the series to remove.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/> which indicates success.
        /// </returns>
        public bool RemoveSeriesName(string name)
        {
            if (this.seriesNames.Contains(name))
            {
                this.seriesNames.Remove(name);
                return true;
            }

            return false;
        }
        #endregion

        /// <summary>
        /// The add data to chart.
        /// </summary>
        /// <param name="x">
        /// The value of x.
        /// </param>
        /// <param name="values">
        /// The multiple y values for x value.
        /// </param>
        public void AddData(float x, List<float> values)
        {
            this.xValues.Add(x);
            if (!this.yValues.ContainsKey(x))
            {
                this.yValues.Add(x, values);
            }
            else
            {
                foreach (float value in values)
                {
                    this.yValues[x].Add(value);
                }
            }
        }

        /// <summary>
        /// The add data to chart.
        /// </summary>
        /// <param name="x">
        /// The value of x.
        /// </param>
        /// <param name="yValue">
        /// The value of y.
        /// </param>
        public void AddData(float x, float yValue)
        {
            this.xValues.Add(x);
            if (!this.yValues.Keys.Contains(x))
            {
                this.yValues.Add(x, new List<float>());
            }

            this.yValues[x].Add(yValue);
        }

        /// <summary>
        /// The add data to chart.
        /// </summary>
        /// <param name="x">
        /// The value of x.
        /// </param>
        /// <param name="yValue">
        /// The value of y.
        /// </param>
        /// <param name="yAxis">
        /// The number of Y axis;
        /// </param>
        /// <exception cref="ArgumentException">
        /// Throws exception when <paramref name="yAxis"/> is wrong.
        /// </exception>
        public void AddData(float x, float yValue, int yAxis)
        {
            if (this.yValues.Count >= yAxis)
            {
                this.yValues[yAxis].Add(yValue);
            }
            else
            {
                throw new ArgumentException(string.Format("Y Axis # {0} doesn't exist.", yAxis));
            }

            this.xValues.Add(x);
        }

        /// <summary>
        /// Draws final layout of chart.
        /// </summary>
        protected override void DrawChart()
        {
            if (this.DisplayLegend)
            {
                this.BuildLegend();
            }

            this.DrawTitle();

            this.AddYAxisTitle();
            this.AddXAxisTitle();
            this.AddTicks();

            this.DrawPoints();
        }

        #region Private methods

        /// <summary>
        /// Configures minimum and maximum values for X and Y axis
        /// if weren't setup
        /// </summary>
        private void SetupMinMaxAxis()
        {
            float toCheck = this.xValues.Min();
            if (this.XAxis.Minimum > toCheck)
            {
                this.XAxis.Minimum = toCheck;
            }

            toCheck = this.xValues.Max();
            if (this.XAxis.Maximum < toCheck)
            {
                this.XAxis.Maximum = toCheck;
            }

            toCheck = this.yValues.Min(x => x.Value.Min());
            if (this.YAxis.Minimum > toCheck)
            {
                this.YAxis.Minimum = toCheck;
            }

            toCheck = this.yValues.Max(x => x.Value.Max());
            if (this.YAxis.Maximum < toCheck)
            {
                this.YAxis.Maximum = toCheck;
            }
        }

        /// <summary>
        /// Prepares chart before adding all elements on it
        /// </summary>
        protected override void PreDraw()
        {
            base.PreDraw();

            this.SetupMinMaxAxis();

            this.XAxis.CalculateScale();
            this.YAxis.CalculateScale();
        }

        /// <summary>
        /// The draw points.
        /// </summary>
        private void DrawPoints()
        {
            this.xValues = this.xValues.Distinct().ToList();
            int loop = this.xValues.Count;

            for (int i = 0; i < loop; i++)
            {
                this._currentColor = 0;
                float xValue = this.xValues[i];
                int group = 0;
                foreach (float value in this.yValues[xValue])
                {
                    Color color = GetNextColor();
                    this.DrawPoint(xValue, value, color, group++);
                }
            }
        }
        #endregion

        /// <summary>
        /// Moves border basing on size of major ticks descriptions.
        /// </summary>
        private void ChangeBorderOnMajorTicksNames()
        {
            if (this.XAxis.DisplayValues)
            {
                string xMiddle =
                    ((this.XAxis.Scale.NiceMax - this.XAxis.Scale.NiceMin) / 2).ToString(
                        CultureInfo.InvariantCulture);
                RectangleF size = DefaultGraphics.Instance.MeasureDisplayString(xMiddle, XAxis.TextStyle.Font);
                this.Border.Bottom -= size.Height + 2;
            }

            if (this.YAxis.DisplayValues)
            {
                string yMiddle =
                    ((this.YAxis.Scale.NiceMax - this.YAxis.Scale.NiceMin) / 2).ToString(
                        CultureInfo.InvariantCulture);
                RectangleF size2 = DefaultGraphics.Instance.MeasureDisplayString(yMiddle, YAxis.TextStyle.Font);
                this.Border.Right -= size2.Height + 2;
            }
        }

        private float AddXAxisTicks(int loop)
        {
            double xValue = this.XAxis.Scale.NiceMin;
            int xMiddle = loop / 2;
            float left = this.Border.Left;
            float distanceFromZero = float.MaxValue;

            double calculate = this.XAxis.Scale.TickSpacing;
            calculate = calculate / (Math.Abs(this.XAxis.Scale.NiceMin) + Math.Abs(this.XAxis.Scale.NiceMax)) * this.Border.Width;

            var spacing = this.XAxis.Scale.TickSpacing;

            if (loop != (int)this.XAxis.Scale.MaxTicks - 1)
            {
                loop = (int)(this.XAxis.Scale.MaxTicks - 1);
                calculate = this.Border.Width / (this.XAxis.Scale.MaxTicks - 1);
            }


            float xTicks = (float)calculate;

            StringFormat sf;
            string minword = this.XAxis.Scale.NiceMin.ToString();
            string maxword = this.XAxis.Scale.NiceMax.ToString();
            var mindigits = GetFractionalDigits(minword);
            var maxdigits = GetFractionalDigits(maxword);
            if (maxdigits > mindigits)
            {
                mindigits = maxdigits;
                minword = maxword;
            }
            if (mindigits == 0)
            {
                mindigits = 1;
            }
            mindigits++;
            //todo: if length of labels bigger than width of border?
            //float length = DefaultGraphics.Instance.MeasureDisplayString(
            //    string.Format(this.XAxis.Format, minword),
            //    this.XAxis.TextStyle.Font).Width;

            //if (length * (loop + 1) > this.Border.Width)
            //{
            //    var flag = 1;
            //}

            for (int i = 0; i <= loop; i++)
            {
                float current;
                if (this.XAxis.OppositeDirection)
                {
                    current = left + (xTicks * (loop - i));
                }
                else
                {
                    current = left + (xTicks * i);
                }

                Line line = new Line(current,
                                     this.Border.Top,
                                     current,
                                     this.Border.Bottom,
                                     new Pen(Color.LightGray));
                this.AddElement(line);

                float distance = (float)Math.Abs(xValue);
                if (distance < distanceFromZero)
                {
                    distanceFromZero = distance;

                    if (this.XAxis.OppositeDirection)
                    {
                        xMiddle = loop - i;
                    }
                    else
                    {
                        xMiddle = i;
                    }
                }

                if (this.XAxis.DisplayValues)
                {
                    var rounded = (float)Math.Round(xValue, mindigits);
                    string text = string.Format(this.XAxis.Format, rounded);
                    ChartString cs = new ChartString(current, this.Border.Bottom + 2, 0, 0,
                        text,
                        this.XAxis.TextStyle.Font, this.XAxis.TextStyle.ColorBrush);
                    cs.Color = this.TextStyle.ColorBrush;

                    sf = new StringFormat();
                    sf.LineAlignment = StringAlignment.Near;
                    if (this.XAxis.OppositeDirection)
                    {
                        if (i == 0)
                        {
                            sf.Alignment = StringAlignment.Far;
                        }
                        else if (i == loop)
                        {
                            sf.Alignment = StringAlignment.Near;
                        }
                        else
                        {
                            sf.Alignment = StringAlignment.Center;
                        }
                    }
                    else
                    {
                        if (i == 0)
                        {
                            sf.Alignment = StringAlignment.Near;
                        }
                        else if (i == loop)
                        {
                            sf.Alignment = StringAlignment.Far;
                        }
                        else
                        {
                            sf.Alignment = StringAlignment.Center;
                        }
                    }

                    cs.Format = sf;
                    this.AddElement(cs);
                }

                // Change values
                xValue += spacing;
            }

            return this.Border.Left + (xMiddle * xTicks);
        }

        public static int GetFractionalDigits(string txt)
        {
            decimal value = decimal.Parse(txt);
            return (decimal.GetBits(value)[3] >> 16) & 0x7fff;
        }

        private float AddYAxisTicks(int loop)
        {
            float top = this.Border.Top;
            int yMiddle = 0;
            float distanceFromZero = float.MaxValue;
            double yValue = this.YAxis.Scale.NiceMax;

            double calculate = this.YAxis.Scale.TickSpacing;
            calculate = calculate / (Math.Abs(this.YAxis.Scale.NiceMin) + Math.Abs(this.YAxis.Scale.NiceMax)) * this.Border.Height;
            float yTicks = (float)calculate;

            StringFormat sf;

            for (int i = 0; i <= loop; i++)
            {
                float current;
                if (this.YAxis.OppositeDirection)
                {
                    current = top + (yTicks * (loop - i));
                }
                else
                {
                    current = top + (yTicks * i);
                }

                Line line = new Line(this.Border.Left, current, this.Border.Right, current, Color.LightGray);
                this.AddElement(line);

                float distance = (float)Math.Abs(yValue);
                if (distance < distanceFromZero)
                {
                    distanceFromZero = distance;
                    if (this.YAxis.OppositeDirection)
                    {
                        yMiddle = loop - i;
                    }
                    else
                    {
                        yMiddle = i;
                    }
                }

                if (this.YAxis.DisplayValues)
                {
                    string text = string.Format(this.XAxis.Format, yValue);

                    ChartString cs = new ChartString(this.Border.Right + 2, current, 0, 0,
                        text,
                        this.YAxis.TextStyle.Font, this.YAxis.TextStyle.ColorBrush);
                    sf = new StringFormat();
                    sf.Alignment = StringAlignment.Near;
                    if (this.YAxis.OppositeDirection)
                    {
                        if (i == 0)
                        {
                            sf.LineAlignment = StringAlignment.Far;
                        }
                        else if (i == loop)
                        {
                            sf.LineAlignment = StringAlignment.Near;
                        }
                        else
                        {
                            sf.LineAlignment = StringAlignment.Center;
                        }
                    }
                    else
                    {
                        if (i == 0)
                        {
                            sf.LineAlignment = StringAlignment.Near;
                        }
                        else if (i == loop)
                        {
                            sf.LineAlignment = StringAlignment.Far;
                        }
                        else
                        {
                            sf.LineAlignment = StringAlignment.Center;
                        }
                    }

                    cs.Format = sf;
                    this.AddElement(cs);
                }
                // Change values
                yValue -= this.YAxis.Scale.TickSpacing;
            }
            
            return this.Border.Top + (yMiddle * yTicks);
        }

        /// <summary>
        /// Adds ticks to chart.
        /// </summary>
        private void AddTicks()
        {
            this.ChangeBorderOnMajorTicksNames();

            int loop =
                (int)((Math.Abs(this.XAxis.Scale.NiceMin) + Math.Abs(this.XAxis.Scale.NiceMax)) / this.XAxis.Scale.TickSpacing);

            float xPosition = this.AddXAxisTicks(loop);

            loop =
                (int)((Math.Abs(this.YAxis.Scale.NiceMin) + Math.Abs(this.YAxis.Scale.NiceMax)) / this.YAxis.Scale.TickSpacing);

            float yPosition = this.AddYAxisTicks(loop);

            this.DrawMajorTicks(xPosition, yPosition);
        }

        /// <summary>
        /// Add major ticks on a chart.
        /// </summary>
        /// <param name="xPosition">
        /// The x position of major tick.
        /// </param>
        /// <param name="yPosition">
        /// The y position of major tick.
        /// </param>
        private void DrawMajorTicks(float xPosition, float yPosition)
        {
            Line line = new Line(xPosition, this.Border.Top, xPosition, this.Border.Bottom,
                                 new Pen(Color.Black, 2));
            this.AddElement(line);

            line = new Line(this.Border.Left, yPosition, this.Border.Right, yPosition,
                            new Pen(Color.Black, 2));
            this.AddElement(line);
        }

        /// <summary>
        /// The build legend.
        /// </summary>
        private void BuildLegend()
        {
            int loops = this.yValues.Max(a => a.Value.Count);

            this.ResetColor();
            List<ValueSet<float>> temp = new List<ValueSet<float>>(this.seriesNames.Count);
            for (int i = 0; i < loops; i++)
            {
                temp.Add(new ValueSet<float>(this.GetNextColor(), this.seriesNames[i], null));
            }

            this.ResetColor();
            float longest = 0;

            switch (this.LegendSettings.Position)
            {
                case Position.BottomRight:
                case Position.MiddleRight:
                case Position.TopRight:
                case Position.Default:
                    longest =
                        DefaultGraphics.Instance.MeasureDisplayString(
                            this.YAxis.Scale.NiceMin.ToString(CultureInfo.InvariantCulture),
                            this.YAxis.TextStyle.Font).Width;
                    float templength = DefaultGraphics.Instance.MeasureDisplayString(
                        this.YAxis.Scale.NiceMin.ToString(CultureInfo.InvariantCulture),
                        this.YAxis.TextStyle.Font).Width;
                    if (templength > longest)
                    {
                        longest = templength;
                    }
                    break;
            }

            this.BuildLegend(this.seriesNames, temp, longest);
        }

        /// <summary>
        /// Add point to draw on chart.
        /// </summary>
        /// <param name="x">
        /// The x position.
        /// </param>
        /// <param name="y">
        /// The y position.
        /// </param>
        /// <param name="color">
        /// The color of point.
        /// </param>
        private void DrawPoint(float x, float y, Color color, int group)
        {
            float yPosition = Math.Abs(this.YAxis.Scale.NiceMin) + Math.Abs(this.YAxis.Scale.NiceMax);
            yPosition = (y + Math.Abs(this.YAxis.Scale.NiceMin)) / yPosition;

            if (this.YAxis.OppositeDirection)
            {
                yPosition = (yPosition * this.Border.Height) + this.Border.Top;
            }
            else
            {
                yPosition = this.Border.Height - (yPosition * this.Border.Height) + this.Border.Top;
            }

            float xPosition = Math.Abs(this.XAxis.Scale.NiceMin) + Math.Abs(this.XAxis.Scale.NiceMax);
            xPosition = (x + Math.Abs(this.XAxis.Scale.NiceMin)) / xPosition;
            

            if (this.XAxis.OppositeDirection)
            {
                xPosition = this.Border.Width - (xPosition * this.Border.Width) + this.Border.Left;
            }
            else
            {
                xPosition = (xPosition * this.Border.Width) + this.Border.Left;
            }

            float size = 8;
            float half = size / 2f;
            Dot point = new Dot(xPosition - half, yPosition - half, size, new SolidBrush(color));
            point.Attributes["g"] = group;
            point.Attributes["data-ot"] = string.Format(CultureInfo.InvariantCulture, "l=;v=[\"{0}, {1}\"]", x, y);
            this.AddElement(point);
        }
        #endregion
    }
}
